﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CutAways.Modeling;
using CutAways.Geometry;
using Tao.OpenGl;

namespace CutAways.Illustration
{
	[Serializable]
	class BoundingTube : BoundingBox
	{
		private Vector backCenter, frontCenter;
		private float radius;

		public override float Radius
		{
			get { return radius; }
		}
		
		public override Vector Center
		{
			get { return Vector.CenterOfLink(backCenter, frontCenter); }
		}
		
		public Vector BackCenter
		{
			get { return backCenter; }
			set { backCenter = value; }
		}

		public Vector FrontCenter
		{
			get { return frontCenter; }
			set { frontCenter = value; }
		}

		public BoundingTube(Vector backCenter, Vector frontCenter, float radius)
		{
			this.backCenter = backCenter;
			this.frontCenter = frontCenter;
			this.radius = radius;
		}

		public void SetRadius(float newRadius)
		{
			this.radius = newRadius;
		}

		public const int STEP_COUNT = 100;

		public override void Draw()
		{
			Draw(Interval.ZeroOne, Interval.ZeroZero);
		}

		public void Draw(Interval axisInterval, Interval angleCutInterval)
		{
			Gl.glPushMatrix();
			Gl.glDisable(Gl.GL_LIGHTING);

			Gl.glBegin(Gl.GL_LINES);

			Vector link = backCenter - frontCenter;
			Vector axis = link;
			axis.Normalize();
			Vector arrow = Vector.SetLength(Vector.Cross(frontCenter - backCenter, Vector.XAxis)/*bacha, mohou byt rovnobezny*/, radius);

			Vector startF, startB;

			Vector f = startF = arrow + frontCenter;
			Vector b = startB = f + link;

			for (int i = 0; i < STEP_COUNT; i++)
			{
				float step = ((float)(Math.PI * 2.0) / (float)STEP_COUNT);
				float angle = i * step;
				
				Vector newf = arrow;
				newf.Rotate(angle, axis);
				newf += frontCenter;
				Vector newb = newf + link;

				if (!angleCutInterval.Contains(angle) && !(angleCutInterval.Maximum > 2f*Math.PI && angle <= angleCutInterval.Maximum - 2f*Math.PI))
				{
					Gl.glVertex3f(f.X, f.Y, f.Z);
					Gl.glVertex3f(b.X, b.Y, b.Z);
				}
				else
				{
					if (!angleCutInterval.Contains(angle - step) && !(angleCutInterval.Maximum > 2f * Math.PI && angle - step <= angleCutInterval.Maximum - 2f * Math.PI))
					{
						Gl.glVertex3f(f.X, f.Y, f.Z);
						Gl.glVertex3f(b.X, b.Y, b.Z);
					}

					Vector cutB = b + (f - b) * axisInterval.Minimum;
					Vector cutF = b + (f - b) * axisInterval.Maximum;
					
					Gl.glVertex3f(f.X, f.Y, f.Z);
					Gl.glVertex3f(cutF.X, cutF.Y, cutF.Z);
					Gl.glVertex3f(cutB.X, cutB.Y, cutB.Z);
					Gl.glVertex3f(b.X, b.Y, b.Z);

					Vector newCutF = newf + (cutF - f);
					Vector newCutB = newb + (cutB - b);
					Gl.glVertex3f(cutF.X, cutF.Y, cutF.Z);
					Gl.glVertex3f(newCutF.X, newCutF.Y, newCutF.Z);

					Gl.glVertex3f(cutB.X, cutB.Y, cutB.Z);
					Gl.glVertex3f(newCutB.X, newCutB.Y, newCutB.Z);
				}
				
				Gl.glVertex3f(f.X, f.Y, f.Z);
				Gl.glVertex3f(newf.X, newf.Y, newf.Z);

				Gl.glVertex3f(b.X, b.Y, b.Z);
				Gl.glVertex3f(newb.X, newb.Y, newb.Z);

				f = newf;
				b = newb;
			}

			Gl.glVertex3f(f.X, f.Y, f.Z);
			Gl.glVertex3f(b.X, b.Y, b.Z);

			Gl.glVertex3f(f.X, f.Y, f.Z);
			Gl.glVertex3f(startF.X, startF.Y, startF.Z);

			Gl.glVertex3f(b.X, b.Y, b.Z);
			Gl.glVertex3f(startB.X, startB.Y, startB.Z);

			Gl.glEnd();

			Gl.glEnable(Gl.GL_LIGHTING);
			Gl.glPopMatrix();
		}

		public static BoundingTube ConvertFrom(OrientedBoundingBox obb)
		{
			return new BoundingTube(obb.Back.GetCenter(), obb.Front.GetCenter(), (obb.Points[0] - obb.Points[5]).GetLength() / 2f);
		}

		public Interval[] IntersectionParametres(OrientedBoundingBox obb, Vector viewpoint)
		{
			Interval axisInterval = Interval.Empty;
			Interval angleInterval = Interval.Empty;

			Vector axis = this.frontCenter - this.backCenter;
			Vector axisNorm = Vector.Norm(axis);
			Vector reference = Vector.Norm(Vector.Cross(axis, Vector.XAxis)); // bod od ktereho se pocitaji uhly rezu
			Vector pivot = (viewpoint - this.backCenter) - (viewpoint - this.backCenter).ProjectionTo(axis); // vektor. ktery smeruje k pozorovateli

			float axisLengthSQR = Vector.SQR(axis.GetLength());

			// nejdriv zjisti, zda objekt neni uvnitr tohoto valce. pokud ano, tak spravne nastav axisInterval
			foreach (Vector inside in obb.Points.Where(p => this.containsPoint(p)))
			{
				float p = Vector.Dot(inside - this.backCenter, axis) / axisLengthSQR;
				axisInterval.Join(p);
			}

			// najdi pruseciky s projekci bodu bounding boxu na valec a spocitej z nich orezavaci parametry
			foreach (Line line in (from p in obb.Points select new Line(p, viewpoint)))
			{
				foreach (Vector inter in line.GetIntersectionsWithCylinderAndItsFloors(this.backCenter, this.frontCenter, this.radius))
				{
					float p = Vector.Dot(inter - this.backCenter, axis) / axisLengthSQR;
					// axis parameter
					axisInterval.Join(p);
					// angle parameter
					Vector projectionToAxis = axis * p;
					float angle = Vector.AngleBetween((inter - this.backCenter) - projectionToAxis, pivot, axisNorm);
					angleInterval.Join(angle);
				}
			}

			// axis interval
			axisInterval = axisInterval.IntersectionWith(Interval.ZeroOne);

			if (axisInterval == Interval.Empty)
				axisInterval = Interval.ZeroZero;
			if (angleInterval == Interval.Empty)
				return new Interval[] { axisInterval, Interval.ZeroZero };


			// angle interval

			if (Math.Abs(angleInterval.Maximum - angleInterval.Minimum) > Math.PI)
				angleInterval = Interval.ZeroTwoPI;
			else
			{
				// jeste ho zrotuju zpet
				float diffAngle = Vector.AngleBetween(pivot, reference, Vector.Norm(axis));
				float a = Vector.GetValidAngle(angleInterval.Maximum + diffAngle);
				float b = Vector.GetValidAngle(angleInterval.Minimum + diffAngle);
				float min = Math.Min(a, b);
				float max = Math.Max(a, b);

				if (angleInterval.Contains(-diffAngle))
				{
					float temp = min;
					min = max;
					max = temp + (float)(2.0 * Math.PI);
				}

				angleInterval = new Interval(min, max);
			}

			return new Interval[] { axisInterval, angleInterval };
		}

		private bool containsPoint(Vector point)
		{
			Vector axis = this.frontCenter - this.backCenter;
			float axisLength = axis.GetLength();
			Vector v = point - this.backCenter;

			float projection = Vector.Dot(v, axis) / axisLength;
			float distance = point.DistanceFromLine(this.backCenter, this.frontCenter);

			return projection >= 0f && projection <= axisLength && distance <= this.radius;
		}
	}
}
